# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2017 David Mandelberg
# Copyright (C) 2017-2018 Sambhav Kothari
# Copyright (C) 2017-2023 Laurent Monin
# Copyright (C) 2018-2023 Philipp Wolfer
# Copyright (C) 2019 Michael Wiencek
# Copyright (C) 2020 dukeyin
# Copyright (C) 2020, 2023, 2025 David Kellner
# Copyright (C) 2021, 2025 Bob Swift
# Copyright (C) 2021 Vladislav Karbovskii
# Copyright (C) 2024 Rakim Middya
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.


from types import SimpleNamespace
from typing import Any

from picard import log
from picard.config import get_config
from picard.const import (
    ALIAS_TYPE_ARTIST_NAME_ID,
    ALIAS_TYPE_LEGAL_NAME_ID,
    RELEASE_FORMATS,
)
from picard.util import (
    format_time,
    linear_combination_of_weights,
    parse_amazon_url,
    translate_from_sortname,
)
from picard.util.script_detector_weighted import detect_script_weighted


_ARTIST_REL_TYPES = {
    'arranger': 'arranger',
    'audio': 'engineer',
    'chorus master': 'performer:chorus master',
    'composer': 'composer',
    'concertmaster': 'performer:concertmaster',
    'conductor': 'conductor',
    'engineer': 'engineer',
    'instrument arranger': 'arranger',
    'librettist': 'lyricist',
    'live sound': 'engineer',
    'lyricist': 'lyricist',
    # 'mastering': 'engineer',
    'mix-DJ': 'djmixer',
    'mix': 'mixer',
    'orchestrator': 'arranger',
    'performing orchestra': 'performer:orchestra',
    'producer': 'producer',
    # 'recording': 'engineer',
    'remixer': 'remixer',
    'sound': 'engineer',
    'audio director': 'director',
    'video director': 'director',
    'vocal arranger': 'arranger',
    'writer': 'writer',
}

_TRACK_TO_METADATA = {
    'number': '~musicbrainz_tracknumber',
    'position': 'tracknumber',
    'title': 'title',
}

_MEDIUM_TO_METADATA = {
    'format': 'media',
    'position': 'discnumber',
    'title': 'discsubtitle',
}

_RECORDING_TO_METADATA = {
    'disambiguation': '~recordingcomment',
    'first-release-date': '~recording_firstreleasedate',
    'title': 'title',
}

_RELEASE_TO_METADATA = {
    'annotation': '~releaseannotation',
    'asin': 'asin',
    'barcode': 'barcode',
    'country': 'releasecountry',
    'date': 'date',
    'disambiguation': '~releasecomment',
    'title': 'album',
}

_ARTIST_TO_METADATA = {
    'disambiguation': '~artistcomment',
    'gender': 'gender',
    'name': 'name',
    'type': 'type',
}

_RELEASE_GROUP_TO_METADATA = {
    'disambiguation': '~releasegroupcomment',
    'first-release-date': '~releasegroup_firstreleasedate',
    'title': '~releasegroup',
}

_PREFIX_ATTRS = {'guest', 'additional', 'minor', 'solo'}
_BLANK_SPECIAL_RELTYPES = {'vocal': 'vocals'}


def _parse_attributes(attrs, reltype, attr_credits):
    prefixes = []
    nouns = []
    for attr in attrs:
        if attr in attr_credits:
            attr = attr_credits[attr]
        if attr in _PREFIX_ATTRS:
            prefixes.append(attr)
        else:
            nouns.append(attr)
    if len(nouns) > 1:
        result = "%s and %s" % (", ".join(nouns[:-1]), nouns[-1:][0])
    elif len(nouns) == 1:
        result = nouns[0]
    else:
        result = _BLANK_SPECIAL_RELTYPES.get(reltype, "")
    prefix = " ".join(prefixes)
    return " ".join([prefix, result]).strip()


def _relation_attributes(relation):
    try:
        return tuple(relation['attributes'])
    except KeyError:
        return tuple()


def _relations_to_metadata_target_type_artist(relation, m, context):
    artist = relation['artist']
    translated_name, sort_name = _translate_artist_node(artist, config=context.config)
    has_translation = translated_name != artist['name']
    if not has_translation and context.use_credited_as and 'target-credit' in relation:
        credited_as = relation['target-credit']
        if credited_as:
            translated_name = credited_as
    reltype = relation['type']
    attribs = _relation_attributes(relation)
    if reltype in {'vocal', 'instrument', 'performer'}:
        if (reltype == 'instrument' and context.use_instrument_credits) or (
            reltype == 'vocal' and context.use_vocal_credits
        ):
            attr_credits = relation.get('attribute-credits', {})
        else:
            attr_credits = {}
        name = 'performer:' + _parse_attributes(attribs, reltype, attr_credits)
    elif reltype == 'mix-DJ' and attribs:
        if not hasattr(m, '_djmix_ars'):
            m._djmix_ars = {}
        for attr in attribs:
            m._djmix_ars.setdefault(attr.split()[1], []).append(translated_name)
        return
    else:
        try:
            name = _ARTIST_REL_TYPES[reltype]
        except KeyError:
            return
    if context.instrumental and name == 'lyricist':
        return
    m.add_unique(name, translated_name)
    if name == 'composer':
        m.add_unique('composersort', sort_name)
    elif name == 'lyricist':
        m.add_unique('~lyricistsort', sort_name)
    elif name == 'writer':
        m.add_unique('~writersort', sort_name)


def _relations_to_metadata_target_type_work(relation, m, context):
    if relation['type'] == 'performance':
        performance_attributes = _relation_attributes(relation)
        for attribute in performance_attributes:
            m.add_unique('~performance_attributes', attribute)
        instrumental = 'instrumental' in performance_attributes
        work_to_metadata(relation['work'], m, instrumental)


def _relations_to_metadata_target_type_url(relation, m, context):
    if relation['type'] == 'amazon asin' and 'asin' not in m:
        amz = parse_amazon_url(relation['url']['resource'])
        if amz is not None:
            m['asin'] = amz['asin']
    elif relation['type'] == 'license':
        url = relation['url']['resource']
        m.add('license', url)


def _relations_to_metadata_target_type_series(relation, m, context):
    if relation['type'] == 'part of':
        entity = context.entity
        series = relation['series']
        var_prefix = f'~{entity}_' if entity else '~'
        name = f'{var_prefix}series'
        mbid = f'{var_prefix}seriesid'
        comment = f'{var_prefix}seriescomment'
        number = f'{var_prefix}seriesnumber'
        if not context.metadata_was_cleared['series']:
            # Clear related metadata first to prevent accumulation
            # of identical value, see PICARD-2700 issue
            m.unset(name)
            m.unset(mbid)
            m.unset(comment)
            m.unset(number)
            # That's to ensure it is done only once
            context.metadata_was_cleared['series'] = True
        m.add(name, series['name'])
        m.add(mbid, series['id'])
        m.add(comment, series['disambiguation'])
        m.add(number, relation['attribute-values'].get('number', ''))


class RelFunc(SimpleNamespace):
    clear_metadata_first = False
    func = None


_RELATIONS_TO_METADATA_TARGET_TYPE_FUNC = {
    'artist': RelFunc(func=_relations_to_metadata_target_type_artist),
    'series': RelFunc(
        func=_relations_to_metadata_target_type_series,
        clear_metadata_first=True,
    ),
    'url': RelFunc(func=_relations_to_metadata_target_type_url),
    'work': RelFunc(func=_relations_to_metadata_target_type_work),
}


def _relations_to_metadata(relations, m, instrumental=False, config=None, entity=None):
    config = config or get_config()
    context = SimpleNamespace(
        config=config,
        entity=entity,
        instrumental=instrumental,
        use_credited_as=not config.setting['standardize_artists'],
        use_instrument_credits=not config.setting['standardize_instruments'],
        use_vocal_credits=not config.setting['standardize_vocals'],
        metadata_was_cleared=dict(),
    )
    for relation in relations:
        target = relation['target-type']
        if target in _RELATIONS_TO_METADATA_TARGET_TYPE_FUNC:
            relfunc = _RELATIONS_TO_METADATA_TARGET_TYPE_FUNC[target]
            if target not in context.metadata_was_cleared:
                context.metadata_was_cleared[target] = not relfunc.clear_metadata_first
            relfunc.func(relation, m, context)


def _locales_from_aliases(aliases):
    def check_higher_score(locale_dict, locale, score):
        return locale not in locale_dict or score > locale_dict[locale][0]

    full_locales = {}
    root_locales = {}
    for alias in aliases:
        if not alias.get('primary'):
            continue
        full_locale = alias.get('locale')
        if not full_locale:
            continue
        root_locale = full_locale.split('_')[0]
        full_parts = []
        root_parts = []
        score = 0.8
        full_parts.append((score, 5))
        if '_' in full_locale:
            score = 0.4
        root_parts.append((score, 5))
        type_id = alias.get('type-id')
        if type_id == ALIAS_TYPE_ARTIST_NAME_ID:
            score = 0.8
        elif type_id == ALIAS_TYPE_LEGAL_NAME_ID:
            score = 0.5
        else:
            # as 2014/09/19, only Artist or Legal names should have the
            # Primary flag
            score = 0.0
        full_parts.append((score, 5))
        root_parts.append((score, 5))
        comb = linear_combination_of_weights(full_parts)
        if check_higher_score(full_locales, full_locale, comb):
            full_locales[full_locale] = (comb, (alias['name'], alias['sort-name']))
        comb = linear_combination_of_weights(root_parts)
        if check_higher_score(root_locales, root_locale, comb):
            root_locales[root_locale] = (comb, (alias['name'], alias['sort-name']))

    return full_locales, root_locales


def _build_alias_locale_maps(aliases: list[dict[str, Any]]) -> tuple[dict[str, str], dict[str, str]]:
    """
    Build maps of full and root locales from alias entries.

    Parameters
    ----------
    aliases : list[dict[str, Any]]
        Alias dictionaries that can include ``name`` and ``locale`` keys.

    Returns
    -------
    tuple[dict[str, str], dict[str, str]]
        A pair ``(full_locales, root_locales)`` where ``full_locales`` maps
        full locale identifiers (e.g., ``"en_US"``) to names and
        ``root_locales`` maps root language codes (e.g., ``"en"``) to names.
    """
    full_locales: dict[str, str] = {}
    root_locales: dict[str, str] = {}
    for alias in aliases:
        locale = alias.get('locale')
        name = alias.get('name')
        if not locale or not name:
            continue
        full_locales.setdefault(locale, name)
        root = locale.split('_')[0]
        # Prefer explicit root-locale aliases (e.g., "en") over values derived
        # from full locales (e.g., "en_US").
        if '_' in locale:
            root_locales.setdefault(root, name)
        else:
            root_locales[root] = name
    return full_locales, root_locales


def _first_match_in_order(order: list[str], mapping: dict[str, str]) -> str | None:
    """
    Return the value for the first key in ``order`` that exists in ``mapping``.

    Parameters
    ----------
    order : list[str]
        Keys to check in priority order.
    mapping : dict[str, str]
        Mapping from key to value to search.

    Returns
    -------
    str or None
        The value corresponding to the first matching key; otherwise ``None``.
    """
    for key in order:
        value = mapping.get(key)
        if value:
            return value
    return None


def _find_localized_alias_name(aliases: list[dict[str, Any]] | None, preferred_locales: list[str]) -> str | None:
    """
    Select a localized alias by preferred locales for non-artist entities.

    The selection prefers an exact locale match first (e.g., ``en_US``),
    then falls back to a root language match (e.g., ``en``).

    Parameters
    ----------
    aliases : list[dict[str, Any]] or None
        Alias entries as provided by the web service, each containing at
        least ``name`` and ``locale``.
    preferred_locales : list[str]
        Ordered list of preferred locales (e.g., ``["en_US", "en"]``).

    Returns
    -------
    str or None
        The localized alias name if a match is found; otherwise ``None``.
    """
    if not aliases:
        return None

    full_locales, root_locales = _build_alias_locale_maps(aliases)

    # 1) Exact full-locale match
    name = _first_match_in_order(preferred_locales, full_locales)
    if name:
        return name

    # 2) Root language match
    root_order = [locale.split('_')[0] for locale in preferred_locales]
    return _first_match_in_order(root_order, root_locales)


def _is_script_exception_enabled(config: Any) -> bool:
    """
    Check whether script-exception matching is enabled.

    Parameters
    ----------
    config : Any
        Configuration object with a ``setting`` mapping.

    Returns
    -------
    bool
        ``True`` if ``translate_artist_names_script_exception`` is enabled,
        otherwise ``False``.
    """
    return config.setting['translate_artist_names_script_exception']


def _get_script_exceptions(config: Any) -> list[tuple[str, int]]:
    """
    Get configured script-exception thresholds.

    Parameters
    ----------
    config : Any
        Configuration object with a ``setting`` mapping.

    Returns
    -------
    list[tuple[str, int]]
        A list of pairs ``(script_id, threshold_percent)``.
    """
    return config.setting['script_exceptions']


def _log_detected_scripts(text: str, detected: dict[str, float]) -> None:
    """
    Log the detected script distribution for the provided text.

    Parameters
    ----------
    text : str
        The text that was analyzed.
    detected : dict[str, float]
        Mapping of ``script_id`` to detected proportion (0.0-1.0).
    """
    if detected:
        parts = "; ".join(f"{script_id} ({detected[script_id] * 100:.1f}%)" for script_id in detected)
    else:
        parts = "None"
    log.debug('Script alpha characters found in "%s": %s', text, parts)


def _matches_script_exception(detected: dict[str, float], exceptions: list[tuple[str, int]]) -> bool:
    """
    Determine if any configured script meets its match threshold.

    Parameters
    ----------
    detected : dict[str, float]
        Mapping of ``script_id`` to detected proportion (0.0–1.0).
    exceptions : list[tuple[str, int]]
        List of pairs ``(script_id, threshold_percent)``.

    Returns
    -------
    bool
        ``True`` if at least one exception script meets or exceeds its
        configured threshold, otherwise ``False``.
    """
    for script_id, script_weighting in exceptions:
        percentage = detected.get(script_id)
        if percentage is None:
            continue
        if percentage >= (script_weighting / 100):
            return True
    return False


def _log_selected_scripts_match(exceptions: list[tuple[str, int]], matched: bool) -> None:
    """
    Log whether any selected scripts matched the detected distribution.

    Parameters
    ----------
    exceptions : list[tuple[str, int]]
        List of pairs ``(script_id, threshold_percent)`` used for matching.
    matched : bool
        Whether a match was found.
    """
    details = "; ".join(f"{scr[0]} ({scr[1]}%)" for scr in exceptions)
    prefix = "Match" if matched else "No match"
    log.debug("%s found in selected scripts: %s", prefix, details)


def _should_skip_translation_due_to_scripts(text: str | None, config: Any | None = None) -> bool:
    """
    Determine whether translation should be skipped based on script exceptions.

    Parameters
    ----------
    text : str or None
        The input text whose writing system scripts will be detected.
    config : Any, optional
        Configuration object providing settings, by default ``None``. If
        ``None``, the global configuration from ``get_config()`` is used.

    Returns
    -------
    bool
        ``True`` if translation should be skipped because the text matches a
        selected script at or above the configured threshold; otherwise
        ``False``.
    """
    config = config or get_config()
    if not text or not _is_script_exception_enabled(config):
        return False

    detected = detect_script_weighted(text)
    _log_detected_scripts(text, detected)
    if not detected:
        return False

    exceptions = _get_script_exceptions(config)
    if not exceptions:
        log.warning("No scripts selected for translation exception match check.")
        return False

    matched = _matches_script_exception(detected, exceptions)
    _log_selected_scripts_match(exceptions, matched)
    return matched


def _translate_artist_node(node, config=None):
    config = config or get_config()
    translated_name, sort_name = None, None
    if config.setting['translate_artist_names']:
        if _should_skip_translation_due_to_scripts(node['name'], config=config):
            return node['name'], node['sort-name']

        # Prepare dictionaries of available locale aliases
        if 'aliases' in node:
            full_locales, root_locales = _locales_from_aliases(node['aliases'])

            # First pass to match full locale if available
            for locale in config.setting['artist_locales']:
                if locale in full_locales:
                    return full_locales[locale][1]

            # Second pass to match root locale if available
            for locale in config.setting['artist_locales']:
                lang = locale.split('_')[0]
                if lang in root_locales:
                    return root_locales[lang][1]

        # No matches found in available alias locales
        sort_name = node['sort-name']
        translated_name = translate_from_sortname(node['name'] or '', sort_name)
    else:
        translated_name, sort_name = node['name'], node['sort-name']
    return (translated_name, sort_name)


def artist_credit_from_node(node):
    artist_name = ''
    artist_sort_name = ''
    artist_names = []
    artist_sort_names = []
    artist_countries = []
    config = get_config()
    use_credited_as = not config.setting['standardize_artists']
    for artist_info in node:
        artist = artist_info['artist']
        if artist and 'id' in artist and artist['id']:
            # Add artist's country code if specified, otherwise 'XX' (Unknown Country)
            artist_countries.append(artist['country'] if 'country' in artist and artist['country'] else 'XX')
        translated_name, sort_name = _translate_artist_node(artist, config=config)
        has_translation = translated_name != artist['name']
        if has_translation:
            name = translated_name
        elif use_credited_as and 'name' in artist_info:
            name = artist_info['name']
        else:
            name = artist['name']
        artist_name += name
        artist_sort_name += sort_name or ''
        artist_names.append(name)
        artist_sort_names.append(sort_name or '')
        if 'joinphrase' in artist_info:
            artist_name += artist_info['joinphrase'] or ''
            artist_sort_name += artist_info['joinphrase'] or ''
    return (artist_name, artist_sort_name, artist_names, artist_sort_names, artist_countries)


def artist_credit_to_metadata(node, m, release=False):
    ids = [n['artist']['id'] for n in node]
    artist_name, artist_sort_name, artist_names, artist_sort_names, artist_countries = artist_credit_from_node(node)
    if release:
        m['musicbrainz_albumartistid'] = ids
        m['albumartist'] = artist_name
        m['albumartistsort'] = artist_sort_name
        m['~albumartists'] = artist_names
        m['~albumartists_sort'] = artist_sort_names
        m['~albumartists_countries'] = artist_countries
    else:
        m['musicbrainz_artistid'] = ids
        m['artist'] = artist_name
        m['artistsort'] = artist_sort_name
        m['artists'] = artist_names
        m['~artists_sort'] = artist_sort_names
        m['~artists_countries'] = artist_countries


def _release_event_iter(node):
    if 'release-events' in node:
        yield from node['release-events']


def _country_from_release_event(release_event):
    try:
        return release_event['area']['iso-3166-1-codes'][0]
    # TypeError in case object is None
    except (KeyError, IndexError, TypeError):
        pass
    return None


def countries_from_node(node):
    countries = []
    for release_event in _release_event_iter(node):
        country_code = _country_from_release_event(release_event)
        if country_code:
            countries.append(country_code)
    return sorted(countries)


def release_dates_and_countries_from_node(node):
    dates = []
    countries = []
    for release_event in _release_event_iter(node):
        dates.append(release_event['date'] or '')
        country_code = _country_from_release_event(release_event)
        if country_code:
            countries.append(country_code)
    return dates, countries


def label_info_from_node(node):
    labels = []
    catalog_numbers = []
    for label_info in node:
        if 'label' in label_info and label_info['label'] and 'name' in label_info['label']:
            label = label_info['label']['name']
            if label and label not in labels:
                labels.append(label)
        if 'catalog-number' in label_info:
            cat_num = label_info['catalog-number']
            if cat_num and cat_num not in catalog_numbers:
                catalog_numbers.append(cat_num)
    return (labels, catalog_numbers)


def media_formats_from_node(node):
    formats_count = {}
    formats_order = []
    for medium in node:
        text = medium.get('format', "(unknown)") or "(unknown)"
        if text in formats_count:
            formats_count[text] += 1
        else:
            formats_count[text] = 1
            formats_order.append(text)
    formats = []
    for medium_format in formats_order:
        count = formats_count[medium_format]
        medium_format = RELEASE_FORMATS.get(medium_format, medium_format)
        if count > 1:
            medium_format = str(count) + "×" + medium_format
        formats.append(medium_format)
    return " + ".join(formats)


def _node_skip_empty_iter(node):
    for key, value in node.items():
        if value or value == 0:
            yield key, value


def track_to_metadata(node, track):
    m = track.metadata
    recording_to_metadata(node['recording'], m, track)
    config = get_config()
    m.add_unique('musicbrainz_trackid', node['id'])
    # overwrite with data we have on the track
    for key, value in _node_skip_empty_iter(node):
        if key == 'title':
            # If translating track titles, keep the title potentially set from recording aliases
            if not config.setting['translate_track_titles']:
                m['title'] = value
        elif key in _TRACK_TO_METADATA:
            m[_TRACK_TO_METADATA[key]] = value
        elif key == 'length' and value:
            m.length = value
        elif key == 'artist-credit':
            artist_credit_to_metadata(value, m)
    if m.length:
        m['~length'] = format_time(m.length)


def recording_to_metadata(node, m, track=None):
    m.length = 0
    m.add_unique('musicbrainz_recordingid', node['id'])
    config = get_config()
    for key, value in _node_skip_empty_iter(node):
        if key in _RECORDING_TO_METADATA:
            m[_RECORDING_TO_METADATA[key]] = value
        elif key == 'user-rating':
            m['~rating'] = value['value']
        elif key == 'length':
            m.length = value
        elif key == 'artist-credit':
            artist_credit_to_metadata(value, m)
            # set tags from artists
            if track:
                for credit in value:
                    artist = credit['artist']
                    artist_obj = track.append_track_artist(artist['id'])
                    add_genres_from_node(artist, artist_obj)
        elif key == 'relations':
            _relations_to_metadata(value, m, config=config, entity='recording')
        elif key == 'isrcs':
            add_isrcs_to_metadata(value, m)
        elif key == 'video' and value:
            m['~video'] = '1'
    add_genres_from_node(node, track)
    # Translate track title from recording aliases if enabled, unless script exception applies
    if config.setting['translate_track_titles']:
        if not _should_skip_translation_due_to_scripts(node.get('title'), config=config):
            alias_name = _find_localized_alias_name(node.get('aliases'), config.setting['artist_locales'])
            if alias_name:
                m['title'] = alias_name
    if m['title']:
        m['~recordingtitle'] = m['title']
    if m.length:
        m['~length'] = format_time(m.length)


def work_to_metadata(work, m, instrumental=False):
    m.add_unique('musicbrainz_workid', work['id'])
    if instrumental:
        m.add_unique('language', 'zxx')  # no lyrics
    elif 'languages' in work:
        for language in work['languages']:
            m.add_unique('language', language)
    elif 'language' in work:
        m.add_unique('language', work['language'])
    if 'title' in work:
        m.add_unique('work', work['title'])
    if 'disambiguation' in work:
        m.add_unique('~workcomment', work['disambiguation'])
    if 'relations' in work:
        _relations_to_metadata(work['relations'], m, instrumental, entity='work')


def medium_to_metadata(node, m):
    for key, value in _node_skip_empty_iter(node):
        if key in _MEDIUM_TO_METADATA:
            m[_MEDIUM_TO_METADATA[key]] = value
    totaltracks = node.get('track-count', 0)
    if node.get('pregap'):
        totaltracks += 1
    if totaltracks:
        m['totaltracks'] = totaltracks


def artist_to_metadata(node, m):
    """Make meatadata dict from a JSON 'artist' node."""
    m.add_unique('musicbrainz_artistid', node['id'])
    for key, value in _node_skip_empty_iter(node):
        if key in _ARTIST_TO_METADATA:
            m[_ARTIST_TO_METADATA[key]] = value
        elif key == 'area':
            m['area'] = value['name']
        elif key == 'life-span':
            if 'begin' in value:
                m['begindate'] = value['begin']
            if 'ended' in value:
                ended = value['ended']
                if ended and 'end' in value:
                    m['enddate'] = value['end']
        elif key == 'begin-area':
            m['beginarea'] = value['name']
        elif key == 'end-area':
            m['endarea'] = value['name']


def release_to_metadata(node, m, album=None):
    """Make metadata dict from a JSON 'release' node."""
    config = get_config()
    m.add_unique('musicbrainz_albumid', node['id'])
    for key, value in _node_skip_empty_iter(node):
        if key in _RELEASE_TO_METADATA:
            m[_RELEASE_TO_METADATA[key]] = value
        elif key == 'status':
            m['releasestatus'] = value.lower()
        elif key == 'artist-credit':
            artist_credit_to_metadata(value, m, release=True)
            # set tags from artists
            if album is not None:
                for credit in value:
                    artist = credit['artist']
                    artist_obj = album.append_album_artist(artist['id'])
                    add_genres_from_node(artist, artist_obj)
        elif key == 'relations' and config.setting['release_ars']:
            _relations_to_metadata(value, m, config=config, entity='release')
        elif key == 'label-info':
            m['label'], m['catalognumber'] = label_info_from_node(value)
        elif key == 'text-representation':
            if 'language' in value:
                m['~releaselanguage'] = value['language']
            if 'script' in value:
                m['script'] = value['script']
    m['~releasecountries'] = release_countries = countries_from_node(node)
    # The MB web service returns the first release country in the country tag.
    # If the user has configured preferred release countries, use the first one
    # if it is one in the complete list of release countries.
    for country in config.setting['preferred_release_countries']:
        if country in release_countries:
            m['releasecountry'] = country
            break
    # Translate album title from release aliases if enabled, unless script exception applies
    if config.setting['translate_album_titles']:
        if not _should_skip_translation_due_to_scripts(node.get('title'), config=config):
            alias_name = _find_localized_alias_name(node.get('aliases'), config.setting['artist_locales'])
            if alias_name:
                m['album'] = alias_name
    add_genres_from_node(node, album)


def release_group_to_metadata(node, m, release_group=None):
    """Make metadata dict from a JSON 'release-group' node taken from inside a 'release' node."""
    config = get_config()
    m.add_unique('musicbrainz_releasegroupid', node['id'])
    for key, value in _node_skip_empty_iter(node):
        if key in _RELEASE_GROUP_TO_METADATA:
            m[_RELEASE_GROUP_TO_METADATA[key]] = value
        elif key == 'primary-type':
            m['~primaryreleasetype'] = value.lower()
        elif key == 'secondary-types':
            add_secondary_release_types(value, m)
        elif key == 'relations' and config.setting['release_ars']:
            _relations_to_metadata(value, m, config=config, entity='releasegroup')
    add_genres_from_node(node, release_group)
    if m['~releasegroup_firstreleasedate']:
        m['originaldate'] = m['~releasegroup_firstreleasedate']
        m['originalyear'] = m['originaldate'][:4]
    m['releasetype'] = m.getall('~primaryreleasetype') + m.getall('~secondaryreleasetype')


def add_secondary_release_types(node, m):
    for secondary_type in node:
        m.add_unique('~secondaryreleasetype', secondary_type.lower())


def add_genres_from_node(node, obj):
    if obj is None:
        return
    if 'tags' in node:
        add_tags(node['tags'], obj)
    if 'user-tags' in node:
        add_user_tags(node['user-tags'], obj)
    if 'genres' in node:
        add_genres(node['genres'], obj)
    if 'user-genres' in node:
        add_user_genres(node['user-genres'], obj)


def add_genres(node, obj):
    for tag in node:
        obj.add_genre(tag['name'], tag['count'])


def add_user_genres(node, obj):
    for tag in node:
        obj.add_genre(tag['name'], 1)


def add_tags(node, obj):
    for tag in node:
        obj.add_folksonomy_tag(tag['name'], tag['count'])


def add_user_tags(node, obj):
    for tag in node:
        obj.add_folksonomy_tag(tag['name'], 1)


def add_isrcs_to_metadata(node, metadata):
    for isrc in node:
        metadata.add('isrc', isrc)


def get_score(node):
    """Returns the score attribute for a node.
    The score is expected to be an integer between 0 and 100, it is returned as
    a value between 0.0 and 1.0. If there is no score attribute or it has an
    invalid value 1.0 will be returned.
    """
    try:
        return int(node.get('score', 100)) / 100
    except (TypeError, ValueError):
        return 1.0
